package mirror

import (
	"context"
	"encoding/json"
	"fmt"
	"io"
	"time"

	"gitee.com/smewl13/registry-sync/v2/config"
	"gitee.com/smewl13/registry-sync/v2/logger"
	"gitee.com/smewl13/registry-sync/v2/mirror/image"
	"github.com/docker/docker/client"
)

type Layer struct {
	MediaType string `json:"mediaType"`
	Digest    string `json:"digest"`
	Size      int64  `json:"size"`
}

type Meta struct {
	SchemaVersion int     `json:"schemaVersion"`
	MediaType     string  `json:"mediaType"`
	Config        Layer   `json:"config"`
	Layers        []Layer `json:"layers"`
}

type Repo struct {
	Repo   string
	Tag    string
	Digest string
	Size   int64
	Local  bool
	Dst    []*config.RegistryServer
}

type ImageTags struct {
	Name string   `json:"name"`
	Tags []string `json:"tags"`
}

type Registry struct {
	Server       *config.RegistryServer
	authUrl      string
	baseUrl      string
	Token        string
	Repositories []string
	Repos        map[string]*Repo
}

func (r *Registry) login() bool {
	var err error
	if r.Token, err = image.Login(r.Server.Tls, r.authUrl); err != nil {
		logger.Errorf("err: %v", err)
		return false
	}
	return true
}

func (r *Registry) getRepos() {
	url := fmt.Sprintf("%s/_catalog", r.baseUrl)
	var data map[string][]string
	if err := json.Unmarshal(image.Request(r.Server.Tls, url, r.Token), &data); err != nil {
		logger.Errorf("%s 获取仓库解析失败: %v", url, err)
		r.Repositories = make([]string, 0)
		return
	}
	r.Repositories = data["repositories"]
}

func (r *Registry) getTags() {
	r.Repos = make(map[string]*Repo)
	for _, repoName := range r.Repositories {
		url := fmt.Sprintf("%s/%s/tags/list", r.baseUrl, repoName)
		var repo ImageTags
		if err := json.Unmarshal(image.Request(r.Server.Tls, url, r.Token), &repo); err != nil {
			logger.Errorf("%s 获取tags解析失败: %v", repoName, err)
			continue
		}

		for _, tagName := range repo.Tags {
			key := fmt.Sprintf("%s:%s", repoName, tagName)
			url := fmt.Sprintf("%s/%s/manifests/%s", r.baseUrl, repoName, tagName)
			resp := image.RequestGet(r.Server.Tls, url, r.Token)
			r.Repos[key] = &Repo{
				Repo:   repoName,
				Tag:    tagName,
				Digest: resp.Header.Get("Docker-Content-Digest"),
				Dst:    make([]*config.RegistryServer, 0),
			}
			body, _ := io.ReadAll(resp.Body)
			meta := new(Meta)
			if err := json.Unmarshal(body, meta); err != nil {
				logger.Errorf("%s:%s 获取元数据解析失败: %v", repoName, tagName, err)
			}

			r.Repos[key].Size += meta.Config.Size
			for _, layer := range meta.Layers {
				r.Repos[key].Size += layer.Size
			}
		}
	}
}

func (r *Registry) imageSync(cli *client.Client, ctx context.Context) {
	start := time.Now()
	noSync, succ, failed := 0, 0, 0
	for repoName, repo := range r.Repos {
		if len(repo.Dst) == 0 {
			noSync++
			continue
		}
		srcImage := fmt.Sprintf("%s/%s", r.Server.Addr, repoName)
		logger.Infof("========== sync %s size: %d starting ==========", repoName, repo.Size)

		func() {
			if !repo.Local {
				err := image.PullImg(cli, ctx, r.Server.User, r.Server.Password, srcImage)
				if err != nil {
					logger.Errorf("获取镜像 %s 失败, err: %v", srcImage, err)
					failed += 1
					return
				}
				logger.Infof("获取镜像 %s 成功", repoName)
				defer image.DeleteImage(cli, ctx, srcImage)
			}
			pushFail := 0
			for _, dst := range repo.Dst {
				dstImage := fmt.Sprintf("%s/%s", dst.Addr, repoName)
				if err := image.RenameTag(cli, ctx, srcImage, dstImage); err != nil {
					logger.Errorf("添加tag: %s 失败, err: %v", dstImage, err)
					pushFail += 1
					continue
				}
				logger.Infof("添加tag: %s 成功", dstImage)
				if err := image.PushImg(cli, ctx, dst.User, dst.Password, dstImage); err != nil {
					logger.Errorf("镜像上传 %s 失败, err: %v", dstImage, err)
					pushFail += 1
					continue
				}
				logger.Infof("镜像上传 %s 成功", dstImage)
			}
			if pushFail > 0 {
				failed += 1
				return
			}
			succ += 1
		}()
	}

	logger.Info("===================================================================")
	logger.Infof(
		"同步结束。总镜像数据: %d, 无须同步镜像数: %d, 同步成功: %d, 同步失败: %d, 开始时间: %s, 用时: %vms",
		len(r.Repos), noSync, succ, failed,
		start.Format("2006-01-02 15:04:05"),
		time.Since(start).Milliseconds(),
	)
	logger.Info("===================================================================")
}

func NewRepos(svr *config.RegistryServer) *Registry {
	protocol := "http"
	if svr.Tls {
		protocol = "https"
	}
	authUrl := fmt.Sprintf("%s://%s:%s@%s/v2", protocol, svr.User, svr.Password, svr.Addr)
	if svr.User == "" && svr.Password == "" {
		authUrl = fmt.Sprintf("%s://%s/v2", protocol, svr.Addr)
	}
	repo := &Registry{
		Server:  svr,
		authUrl: authUrl,
		baseUrl: fmt.Sprintf("%s://%s/v2", protocol, svr.Addr),
	}
	if !repo.login() {
		return nil
	}
	repo.getRepos()
	repo.getTags()
	return repo
}

func getTarTags() []*Registry {
	dsts := make([]*Registry, 0, len(config.Targets))
	for _, tartag := range config.Targets {
		repo := NewRepos(tartag)
		if repo == nil {
			continue
		}
		dsts = append(dsts, repo)
	}
	return dsts
}

func getSyncInfo(cli *client.Client, ctx context.Context, src *Registry, dsts []*Registry) {
	for repoName, repo := range src.Repos {
		imageName := fmt.Sprintf("%s/%s", src.Server.Addr, repoName)
		repoDigest := fmt.Sprintf("%s/%s@%s", src.Server.Addr, repo.Repo, repo.Digest)
		inspect := image.GetImageInfo(cli, ctx, imageName)
		if inspect != nil && inspect.RepoDigests[0] == repoDigest {
			repo.Local = true
		}
		for _, dst := range dsts {
			dstCopy := dst
			dstRepo, ok := dstCopy.Repos[repoName]
			if ok && repo.Digest == dstRepo.Digest {
				continue
			}
			repo.Dst = append(repo.Dst, dstCopy.Server)
		}
	}
}

func Run() {
	logger.Infof("src config: %+v", config.Source)
	targets := make([]string, 0, len(config.Targets))
	for _, target := range config.Targets {
		targets = append(targets, fmt.Sprintf("%+v", *target))
	}
	logger.Infof("dst config: %+v", targets)

	for {
		func() {
			src := NewRepos(config.Source)
			if src == nil {
				return
			}
			cli, err := image.DockerCli()
			if err != nil {
				logger.Errorf("连接docker失败: %v", err)
				return
			}
			defer cli.Close()
			ctx := context.Background()
			dsts := getTarTags()
			getSyncInfo(cli, ctx, src, dsts)
			src.imageSync(cli, ctx)
		}()

		time.Sleep(time.Minute * time.Duration(config.Interval))
	}
}
